Server-Sent Events

tasos@kadena.io

September 29, 2023

whoami

whois "Tasos Bitsios"

  • Developer @ Kadena Developer Experience team
  • Full stack software developer ~ 13 years
    • Somwehat backend-leaning
    • Mostly JS/TS/node.js/React
    • Mostly worked in startups
    • Mostly harmless
  • Socials:

whois Kadena

Server-Sent Events (SSE)

A server-push protocol

Use cases

Replaces polling.

Stream any kind of update from the server.

What is this new thing?

🥳 SSE is 19 years old

🔧 13 years of mainstream support


W3C Publication History


Current: HTML Living Standard § 9.2

Playground repo - download me!

Play-along repository; basic SSE server & React app:

https://github.com/takadenoshi/sse-presentation

Useful for examining behaviors, browser implementation differences.

Repo link in QR ➡

Minimum Viable SSE response

The simplest server-sent event stream specifies just data events.

Example with 2 events:

> GET /stream/hello HTTP/1.1

< HTTP/1.1 200 OK
< Content-Type: text/event-stream

< data: Hello\n\n

< data: ReactLive are you there?\n\n

Content-Type is text/event-stream

Data is encoded in UTF-8 (mandatory)

Events separated by two newline characters \n\n


Playground: “simple” scenario

Simple EventSource consumer

Server-sent events are consumed with EventSource:

let i=0;

const source = new EventSource("http://localhost:3001/stream/simple");

// "message" event emitted for each "data" event received
source.addEventListener("message", (event) => console.log(++i, event.data), false);

The “minimum viable response” from the previous slide would trigger the callback twice, logging:

1 Hello
2 ReactLive are you there?

Named Events

You can “namespace” your events using the event: field with any custom name:

< event: goal
< data: "ARS-LIV 1-1 45"\n\n

< event: spectator-chat
< data: "Did you see that ludicrous display just now"\n\n

The goal and spectator-chat events are handled separately on the frontend

Allows multiplexing / routing events without need for pattern matching on the data payload. (e.g. a .type field in a JSON object)

Comments

Any lines starting with : (colon) are interpreted as comments

< data: this or that\n\n

< :TODO emit some events in the near future

These are ignored on the client-side

Reconnection (1)

By default*, EventSource consumers will reconnect if the connection is interrupted.

* with implementation-specific caveats

The default reconnection timeout is up to each browser (empirically: between 3-5 s.)

Custom timeouts

The reconnection timeout can be customized from the server-side by emitting a retry: field in any of the events.

retry: 2500
data: Hello!\n\n

Value is in ms.

Timeouts are linear.

Playground: “Retry-flaky” scenario

Reconnection (2)

Computer can say no

A server can signal “do not reconnect”:

Playground: “Not SSE” scenario

Reconnection (3) - Last-Event-ID

Events can include an id field with any UTF-8 string as value.

If the connection is interrupted, the last received ID is sent to the server (header Last-Event-ID)

This allows the server to resume from the client’s last known message.


If this is the last event received in a stream that disconnects:

id: data-0
retry: 5000
data: Data Zero event\n\n

Then the connection timeout will be 5 seconds, and when reconnecting the Last-Event-ID header will be set to data-0:

> GET /stream/notifications HTTP/1.1
> Host: localhost:3001
> Last-Event-ID: data-0

Playground: “notifications” scenario

Full SSE response

> GET /stream/hello HTTP/1.1

< HTTP/1.1 200 OK
< Content-Type: text/event-stream

< retry: 2000
< id: 0
< data: Hello\n\n

< :I am a comment line\n\n

< id: 1
< event: status
< data: {"L":"warning","M":"Service degraded"}\n\n





client-side reconnect after 2s



no client-side effect


custom event named "status"

These 4+1 fields are the entire SSE grammar

More EventSource consumer

You can subscribe to custom events (e.g. status) with .addEventListener:

const source = new EventSource('/stream/hello');

// [name]: triggers for custom named event, here: "status"
source.addEventListener(
  "status",
  ({ data }) => console.log("custom event: status", JSON.parse(data)),
  false,
);

// message: on generic/unnamed "data" events, as before
source.addEventListener("message", (event) => { console.log("received data event", event.data); }, false);

Subscribe to open and error for connection management:

// open: on connection established
source.addEventListener("open", (event) => { console.log("Connection opened"); }, false);

// error: on error/disconnection. sadly entirely devoid of detail
source.addEventListener("error", (event) => { console.log("Connection error"); }, false);

The EventSource Interface

MDN Reference

Error event is a bit useless

Implementation Considerations: HTTP/1.1 connections quota

Per MDN:

Warning: When not used over HTTP/2, SSE suffers from a limitation to the maximum number of open connections, which can be especially painful when opening multiple tabs, as the limit is per browser and is set to a very low number (6). The issue has been marked as “Won’t fix” in Chrome and Firefox.

This limit is per browser + domain, which means that you can open 6 SSE connections across all of the tabs to www.example1.com and another 6 SSE connections to www.example2.com (per Stackoverflow).

When using HTTP/2, the maximum number of simultaneous HTTP streams is negotiated between the server and the client (defaults to 100).


Solutions

Implementation Considerations: Reconnecting

Default reconnection behavior implementation is not fully standardized

Consider handling reconnections yourself:


Test it out in the playground repo:

Implementation Considerations: Proxies (1)

Proxies can kill

Proxies, load balancers and other networking middleware can kill idle connections after a short while.


Two approaches to fix this:

1/ Comment (not client-aware)

You can emit a comment (any line starting with a colon :)

< :bump

Good enough to keep connection alive.

EventSource won’t emit any event.

Implementation Considerations: Proxies (2)

Proxies can kill

Proxies, load balancers and other networking middleware can kill idle connections after a short while.


Two approaches to fix this:

2/ Heartbeat event (client-aware)

Emit a custom “heartbeat” or “ping” event every 15 seconds or so:

event: heartbeat
data: ""

(data field must be present)

The client can listen to this event and use it to detect stale connections:

“expect heartbeats every N seconds, otherwise reconnect”

Preferred approach, especially for important payloads.

Implementation Considerations: Service Workers (Firefox)

Firefox Service Workers 💔 EventSource

Firefox has yet to implement support for EventSource in its Service Worker context.

✅ You can use it in a SharedWorker

Future people can track the present validity of this statement here.

Implementation Considerations: Last-Event-ID detail

If no event is emitted in the subsequent connection’s lifetime, the Last-Event-ID is reset.

When a reconnected session is initialized with Last-Event-ID: X

And the connection emits no messages for its lifetime,

Then the Last-Event-ID value is reset.

Vs competiting options

Widely supported options for polling/streaming updates:

1a/ Polling

Keep requesting new data on an interval

  • Slower
  • Usually more resource intensive than SSE

Benefit: Doesn’t “hog” a connection (HTTP/1.1)

1b/ Long Polling

“Hanging GET” - server keeps connection open/hanging until there is something to write.

Client loops the GET request.

2/ SSE

  • Like formalized, reusable long polling
  • HTTP + REST compatible
    • Works with your existing framework
    • Works with your existing auth
  • Reconnecting

3/ Websockets

  • Not HTTP/REST
    • Websocket server usually a separate stack inside/beside your main backend
  • Must bring your own:
    • Routing
    • Auth
    • Error handling
    • TLS Certs (duplicated)
  • Pain to debug
  • Bidirectional/full duplex
    • good if you need it
    • overkill for

Can I use?

Yes (96.11%)

https://caniuse.com/eventsource

References

§ 9.1 MessageEvent Interface - HTML Living Standard

§ 9.2 Server-Sent Events - HTML Living Standard

This presentation & accompanying playground - Github